package main

import (
	"bytes"
	"database/sql"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	nested "github.com/antonfisher/nested-logrus-formatter"
	log "github.com/sirupsen/logrus"
	"gopkg.in/cheggaaa/pb.v1"

	tile3d "github.com/flywave/go-3dtile"
	_ "github.com/shaxbee/go-spatialite"
)

var help bool
var baseurl string
var chanNums int
var depth bool
var maxlevel int

var errorcnt int64
var maxerrorcnt int64 = 1024

var level int

var cnter int64
var db *sql.DB
var wg sync.WaitGroup
var workers chan string

var bar *pb.ProgressBar

//fetchTileset get tileset.json
func fetchTileset(uri string, depth bool) {
	defer wg.Done()
	defer func() {
		<-workers
	}()
	_, err := url.Parse(uri)
	if err != nil {
		log.Errorf("parse :%s error, details: %s ~", uri, err)
		return
	}
	time.Sleep(time.Millisecond * 300)
	t := time.Now()
	client := http.Client{
		Timeout: 10 * time.Second,
	}
	resp, err := client.Get(baseurl + uri)
	if err != nil {
		log.Errorf("fetch :%s error, details: %s ~", uri, err)
		errorcnt++
		if errorcnt > maxerrorcnt {
			log.Fatal("timeout 服务器可能要崩~~~")
		}
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		log.Errorf("fetch %v tileset error, status code: %d ~", uri, resp.StatusCode)
		return
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Errorf("read tileset error ~ %s", err)
		return
	}
	if len(body) == 0 {
		log.Warnf("nil tileset %v ~", uri)
		return
	}

	// rawp := u.EscapedPath()
	// dir := filepath.Join(basedir, filepath.Dir(rawp))
	// os.MkdirAll(dir, os.ModePerm)
	// fileName := filepath.Join(basedir, rawp)
	// err = ioutil.WriteFile(fileName, body, os.ModePerm)
	// if err != nil {
	// 	log.Warnf("write tileset %v ~", err)
	// }
	if depth {
		_, err = db.Exec("INSERT INTO tiles(u,d,t,s,c) values(?,?,?,?,?)", uri, body, 1, 0, 1)
		if err != nil {
			log.Warnf("insert tileset %s error, details:%v", uri, err)
		}
	}
	bdReader := bytes.NewReader(body)
	ts := tile3d.TilesetFromJson(bdReader)
	atomic.StoreInt64(&cnter, 0)
	for _, child := range ts.Root.Children {
		procChild(uri, child, depth)
	}

	_, err = db.Exec("UPDATE tiles SET s = ? , d = ? WHERE u = ?", cnter, body, uri)
	if err != nil {
		log.Errorf("update tileset %s status error, detail: %v", uri, err)
	}

	secs := time.Since(t).Seconds()
	log.Infof("fetch %s, %.3fs, %.2f kb, %s ...\n", getOct(uri), secs, float32(len(body))/1024.0, uri)
}

//FetchTile get tile
func fetchTile(uri string) {
	//"OR REPLACE/IGNORE for replace"
	_, err := db.Exec("INSERT OR IGNORE INTO tiles(u,d,t,s,c) values(?,?,?,?,?)", uri, []byte{}, 2, 0, 1)
	if err != nil {
		log.Warnf("INSERT %s error, details:%v", uri, err)
		return
	}
	_, err = url.Parse(uri)
	if err != nil {
		log.Errorf("parse :%s error, details: %s ~", uri, err)
		return
	}
	time.Sleep(time.Millisecond * 300)
	t := time.Now()
	client := http.Client{
		Timeout: 10 * time.Second,
	}
	resp, err := client.Get(baseurl + uri)
	if err != nil {
		log.Errorf("fetch :%s error, details: %s ~", uri, err)
		errorcnt++
		if errorcnt > maxerrorcnt {
			log.Fatal("timeout 服务器可能要崩~~~")
		}
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		log.Errorf("fetch %v tile error, status code: %d ~", uri, resp.StatusCode)
		return
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Errorf("read tile error ~ %s", err)
		return
	}
	if len(body) == 0 {
		log.Warnf("nil tile %v ~", uri)
		return
	}

	// rawp := u.EscapedPath()
	// dir := filepath.Join(basedir, filepath.Dir(rawp))
	// os.MkdirAll(dir, os.ModePerm)
	// fileName := filepath.Join(basedir, rawp)
	// err = ioutil.WriteFile(fileName, body, os.ModePerm)
	// if err != nil {
	// 	log.Warnf("write tiles %v ~", err)
	// }

	_, err = db.Exec("UPDATE tiles SET d = ? ,s = ? WHERE u = ?", body, 1, uri)
	if err != nil {
		log.Warnf("UPDATE %s error, details:%v", uri, err)
		return
	}
	atomic.AddInt64(&cnter, 1)
	secs := time.Since(t).Seconds()
	log.Infof("fetch %s , %.3fs, %.2f kb, %s ...\n", getOct(uri), secs, float32(len(body))/1024.0, uri)
}

func procChild(baseuri string, tile tile3d.Tile, depth bool) {

	if tile.Content != nil {
		procContent(baseuri, tile.Content.Url, depth)
	}

	for _, child := range tile.Children {

		if child.Content != nil {
			procContent(baseuri, child.Content.Url, depth)
		}

		if child.Children != nil {
			for _, c := range child.Children {
				procChild(baseuri, c, depth)
			}
		}
	}
}

func procContent(buri, curi string, depth bool) {
	bar.Increment()
	bu, err := url.Parse(buri)
	if err != nil {
		log.Error(err)
	}
	cu, err := url.Parse(curi)
	if err != nil {
		log.Error(err)
	}
	ru := bu.ResolveReference(cu)
	uri := ru.Path
	ext := filepath.Ext(cu.Path)
	switch strings.ToLower(ext) {
	case ".json":

		if level := getLevel(uri); level >= maxlevel {
			log.Warnf("uri:%s, level:%d, maxlevel:%d \n", uri, level, maxlevel)
			break
		}

		if depth {
			// fetchTileset(uri, depth)
			select {
			case workers <- uri:
				wg.Add(1)
				go fetchTileset(uri, depth)
			}
		} else {
			cnt := countTileset(uri)
			_, err = db.Exec("INSERT OR IGNORE INTO tiles(u,d,t,s,c) values(?,?,?,?,?)", uri, nil, 1, 0, cnt)
			if err != nil {
				log.Warnf("insert tileset  %s error, details: %v", uri, err)
				return
			}
			atomic.AddInt64(&cnter, 1)
		}
	case ".b3dm", ".pnts", ".i3dm":
		if level := getLevel(uri); level <= maxlevel {
			fetchTile(uri)
		} else {
			log.Infof("uri:%s, level:%d, maxlevel:%d \n", uri, level, maxlevel)
		}
	}
}

//countTileset get tileset.json
func countTileset(uri string) int {
	var cnt int
	client := http.Client{
		Timeout: 10 * time.Second,
	}
	resp, err := client.Get(baseurl + uri)
	if err != nil {
		log.Errorf("fetch :%s error, details: %s ~", uri, err)
		return cnt
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		log.Errorf("fetch %v tileset error, status code: %d ~", uri, resp.StatusCode)
		return cnt
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Errorf("read tileset error ~ %s", err)
		return cnt
	}
	if len(body) == 0 {
		log.Warnf("nil tileset %v ~", uri)
		return cnt
	}
	bdReader := bytes.NewReader(body)
	ts := tile3d.TilesetFromJson(bdReader)
	for _, child := range ts.Root.Children {
		cnt += countChild(child)
	}
	return cnt
}

func countChild(tile tile3d.Tile) int {
	var cnt int
	if tile.Content != nil {
		cnt++
	}

	for _, child := range tile.Children {
		if child.Content != nil {
			cnt++
		}

		if child.Children != nil {
			for _, c := range child.Children {
				cnt += countChild(c)
			}
		}
	}
	return cnt
}

func startFetcher(depth bool) {
	rows, err := db.Query(`SELECT t1.u,t1.c 
	FROM 
	tiles t1,
	(SELECT LENGTH(u) ulength FROM tiles WHERE t=1 AND s=0  ORDER BY LENGTH(u) LIMIT 1) t2 
	WHERE t1.t=1 AND t1.s=0 AND LENGTH(t1.u)= t2.ulength ;`)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	var setlist []string
	var url string
	var total int
	for rows.Next() {
		cnt := 0
		rows.Scan(&url, &cnt)
		total += cnt
		setlist = append(setlist, url)
	}

	if len(setlist) == 0 {
		return
	}

	prestr := ""
	if depth {
		log.Info("start dfs fetcher ~")
		prestr = "dfs fetcher"
	} else {
		level = getLevel(url)
		log.Infof("start bfs fetcher, level:%d ~", level)
		prestr = fmt.Sprintf("bfs fetcher, level:%d", level)
	}

	bar = pb.New(total).Prefix(prestr).Postfix("\n")
	bar.SetRefreshRate(time.Second)
	bar.Start()

	workers = make(chan string, chanNums)

	for _, url := range setlist {
		// fetchTileset(url, depth)
		select {
		case workers <- url:
			wg.Add(1)
			go fetchTileset(url, depth)
		}
	}
	wg.Wait()
	bar.FinishPrint("finished ~")

	if !depth {
		level++
		startFetcher(depth)
	}
}

func reFetcher(depth bool) {
	log.Info("starting refetcher...")
	rows, err := db.Query("SELECT u FROM tiles WHERE t=2 and s=0;")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	var setlist []string
	var url string
	for rows.Next() {
		rows.Scan(&url)
		setlist = append(setlist, url)
	}

	prestr := ""
	if depth {
		log.Info("start refetcher ~")
		prestr = "refetcher"
	} else {
		level = getLevel(url)
		log.Infof("start refetcher, level:%d ~", level)
		prestr = fmt.Sprintf("refetcher, level:%d", level)
	}

	bar = pb.New(len(setlist)).Prefix(prestr).Postfix("\n")
	bar.SetRefreshRate(time.Second)
	bar.Start()

	atomic.StoreInt64(&cnter, 0)
	// workers = make(chan string, chanNums)
	for _, url := range setlist {
		fetchTile(url)
		bar.Increment()
		// select {
		// case workers <- url:
		// 	wg.Add(1)
		// 	go fetchTile(url)
		// }
	}
	// wg.Wait()

	//如果全部获取成功，则更新所有json的s==c,否则暂停程序
	if int(cnter) == len(setlist) {
		_, err := db.Exec(`UPDATE tiles SET s=c WHERE s != 0 AND s < c AND t = 1 ;`)
		if err != nil {
			log.Fatal(err)
		}
	} else {
		log.Fatalf("重新获取上次失败的b3dm不完成，请检查后重试，上次：%d，本次: %d", len(setlist), cnter)
	}

	bar.FinishPrint("refetcher finished ~")
}

func main() {

	flag.BoolVar(&help, "h", false, "Show this help")
	flag.BoolVar(&depth, "dfs", false, "enabel dfs, default is bfs")
	flag.IntVar(&chanNums, "c", 3, "Specify number of fetchers")
	flag.IntVar(&maxlevel, "max", 22, "Specify max level to stop")

	flag.Usage = usage
	flag.Parse()
	if help {
		flag.Usage()
	}
	args := flag.Args()
	if len(args) < 1 {
		fmt.Fprintf(os.Stderr, "Please specify the root tileset url ~\n")
		flag.Usage()
		return
	}
	u, err := url.Parse(args[0])
	if err != nil {
		log.Error(err)
		return
	}
	baseurl = u.Scheme + "://" + u.Host

	log.SetFormatter(&nested.Formatter{
		HideKeys:        true,
		ShowFullLevel:   true,
		TimestampFormat: "2006-01-02 15:04:05.000",
	})

	fpath := strings.ReplaceAll(u.EscapedPath()[1:], "/", "_")
	dbname := strings.ReplaceAll(fpath, filepath.Ext(fpath), ".db")
	db, err = sql.Open("sqlite3", dbname)
	if err != nil {
		log.Fatal(err)
	}
	// _, err = db.Exec("PRAGMA synchronous=0")
	// if err != nil {
	// 	log.Fatal(err)
	// }
	// _, err = db.Exec("PRAGMA locking_mode=EXCLUSIVE")
	// if err != nil {
	// 	log.Fatal(err)
	// }
	// _, err = db.Exec("PRAGMA journal_mode=DELETE")
	// if err != nil {
	// 	log.Fatal(err)
	// }

	_, err = db.Exec("create table if not exists tiles (u text NOT NULL PRIMARY KEY, d blob, t integer, s integer,c integer);")
	if err != nil {
		log.Fatal(err)
	}
	start := time.Now()
	var s, c int
	err = db.QueryRow("SELECT s,c FROM tiles WHERE t=1 and u= ?;", u.Path).Scan(&s, &c)
	if err == sql.ErrNoRows {
		cnt := countTileset(u.Path)
		_, err = db.Exec("INSERT INTO tiles(u,d,t,s,c) values(?,?,?,?,?)", u.Path, nil, 1, 0, cnt)
		if err != nil {
			log.Warnf("insert tileset %s error, details:%v", u.Path, err)
		}
		fmt.Printf("url:%s, children:%d, status:%d\n", u.Path, cnt, s)
	} else if err != nil {
		fmt.Printf("query url error, details: %v\n", err)
	} else {
		if s == c {
			fmt.Printf(" %s already fetched\n", u.Path)
		} else {
			fmt.Printf("url:%s, children:%d, status:%d\n", u.Path, c, s)
		}
	}
	//先refetcher上次失败的数据
	reFetcher(depth)
	//再开始新的下载
	startFetcher(depth)
	log.Printf("finished, consuming : %fs\n", time.Since(start).Seconds())
}

func usage() {
	fmt.Fprintf(os.Stderr, `3dtbox version: 3dtbox/0.0.1
Usage: 3dtbox [-h] [-o directory] url

Options:
`)
	flag.PrintDefaults()
}

func getLevel(uri string) int {
	parts := strings.Split(filepath.Base(uri), "|")
	if len(parts) > 1 {
		preOct := parts[1]
		if strings.HasPrefix(preOct, "1s") {
			return len(preOct[2:])
		}
	}
	return 0
}

func getOct(uri string) string {
	parts := strings.Split(filepath.Base(uri), "|")
	if len(parts) > 1 {
		return parts[1]
	}
	return "nil oct"
}
